//Copyright (c) 2010 Christopher E. S. King (http://chriskingconsulting.com)
//
//Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files 
// (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, 
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do 
// so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE 
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
// WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Xml.Linq;
using System.Collections;
using System.Reflection;
using System.Runtime.Serialization;
using System.Reflection.Emit;
using System.Text.RegularExpressions;

using King.Infoset;
using King.Infoset.Schema;
using King.Infoset.Reflection;
using King.Extensions;
using King.Reflection;

using ParseLambda = System.Func<string, object>;
using ToStringLambda = System.Func<object, string>;
using FactoryLambda = System.Func<object>;

namespace King.Infoset.Serialization {

    internal static partial class Constants {
        internal static XNamespace XsdXNamespace            = "http://www.w3.org/2001/XMLSchema";
        internal static XNamespace KingInfosetXNamespace   = "http://service.king.com/infoset";
        internal static XName XNull                         = KingInfosetXNamespace + "null";
        internal static XName XEnumerable                   = KingInfosetXNamespace + "enumerable";

        internal static XName Integer                       = KingInfosetXNamespace + "integer";
        internal static XName BigInteger                    = KingInfosetXNamespace + "bigInteger";
        internal static XName Boolean                       = KingInfosetXNamespace + "boolean";
        internal static XName Date                          = KingInfosetXNamespace + "date";
        internal static XName Double                        = KingInfosetXNamespace + "double";
        internal static XName String                        = KingInfosetXNamespace + "string";
        internal static XName Object                        = KingInfosetXNamespace + "object";
        internal static XName Enumeration                   = KingInfosetXNamespace + "enumeration";

        internal static XName XsdInt32                      = XsdXNamespace + "integer";
        internal static XName XsdInt64                      = XsdXNamespace + "long";
        internal static XName XsdBool                       = XsdXNamespace + "boolean";
        internal static XName XsdDateTime                   = XsdXNamespace + "string";
        internal static XName XsdDouble                     = XsdXNamespace + "double";
        internal static XName XsdString                     = XsdXNamespace + "string";

        internal static BindingFlags PrivateStatic = BindingFlags.NonPublic | BindingFlags.Static;
        internal static BindingFlags PrivateInstance = BindingFlags.NonPublic | BindingFlags.Instance;
        internal static BindingFlags InstanceDeclaredOnly = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.DeclaredOnly;
        internal static BindingFlags Instance = BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic;
    }

    public delegate object FactoryDelegate(XElement xTarget);

    public sealed class Serializer {

        #region Static Public Members
        public static void AddRename(object version, XName oldName, XName newName) {
            Dictionary<XName, XName> renameSerialize;
            if (!s_renameSerialize.TryGetValue(version, out renameSerialize))
                s_renameSerialize[version] = renameSerialize = new Dictionary<XName, XName>();

            Dictionary<XName, XName> renameDeserialize;
            if (!s_renameDeserialize.TryGetValue(version, out renameDeserialize))
                s_renameDeserialize[version] = renameDeserialize = new Dictionary<XName, XName>();

            renameSerialize[newName] = oldName;
            renameDeserialize[oldName] = newName;
        }
        #endregion

        #region Static Constructor
        static Serializer() {
            s_renameSerialize = new Dictionary<object, Dictionary<XName, XName>>();
            s_renameDeserialize = new Dictionary<object, Dictionary<XName, XName>>();
        }
        #endregion

        #region Static Private Data Members
        private static Dictionary<object, Dictionary<XName, XName>> s_renameSerialize;
        private static Dictionary<object, Dictionary<XName, XName>> s_renameDeserialize;
        #endregion

        #region Private Data Members
        private object m_version;
        private Dictionary<XName, XName> m_renameSerialize;
        private Dictionary<XName, XName> m_renameDeserialize;
        #endregion

        #region Constructor
        public Serializer()
            : this(new object()) {
        }
        public Serializer(object version) {
            m_version = version;
        }
        #endregion

        #region Private Members
        private XName RenameSerialize(XName newName) {
            if (m_renameSerialize == null) {
                if (!s_renameSerialize.TryGetValue(m_version, out m_renameSerialize))
                    return newName;
            }
                
            XName oldName;
            if (!m_renameSerialize.TryGetValue(newName, out oldName))
                return newName;

            return oldName;
        }
        private XName RenameDeserialize(XName oldName) {
            if (m_renameDeserialize == null) {
                if (!s_renameDeserialize.TryGetValue(m_version, out m_renameDeserialize))
                    return oldName;
            }

            XName newName;
            if (!m_renameDeserialize.TryGetValue(oldName, out newName))
                return oldName;

            return newName;
        }

        private Exception Exception(XElement targetXml, string format, params string[] arguments) {
            var xpath = targetXml.ToLinkList(o => o.Parent).Reverse().StringJoin("/", o => o.Name.LocalName);
            var message = "Error during deserialization of element " + xpath + ": " + format;
            if (arguments.Any())
                message = message.Substitute(arguments);
            return new ArgumentException(message);
        }
        private object Deserialize(
            // the xml to be deserialized
            XElement targetXml,
            // the Contract of the location where the deserialized object will be stored            
            XsContract locationContract,
            // the ContractMember where the deserialized object will be stored            
            XsMember locationMember) {

            KingDebug.Assert(targetXml != null);

            var typedObjectResult = false;
            var locationIsEnumeration = false;
            var locationIsObject = false;
            var deferDeserialization = false;

            if (locationMember != null) {
                typedObjectResult = locationMember.TypedObjectResult;
                locationIsObject = locationMember.Contract.IsObject;
                locationIsEnumeration = locationMember.Contract.IsEnumeration;
                deferDeserialization = locationMember.DeferDeserialization;
            }

            // resolve the target contract. Object locations have self typeing targets
            var targetContract = locationContract;
            if (locationContract.IsObject) {
                targetContract = XsLoader.GetContractOrNull(RenameDeserialize(targetXml.Name));
                if (targetContract == null)
                    throw Exception(targetXml, "Element '" + targetXml.Name + "' is unrecognized");
                
                targetContract = targetContract.MakeRequired();

                // verify the resolution succeded
                if (targetContract == null)
                    throw Exception(targetXml, "Element '" + targetXml.Name.LocalName +
                        "' cannot be resolved to a known type.");

                // verify the contract matches a constraint
                var constraints = locationContract.Constraints();
                if (constraints.Any() && constraints.None(o => o == targetContract || targetContract.IsDerivationOf(o)))
                    throw Exception(targetXml, "Element '" + targetXml.Name.LocalName +
                        "' must be of a type in this set: " + constraints.StringJoin(", ", o => o.Name));
            }

            // xml
            if (targetContract.IsXml) 
                return targetXml;

            // enumeration
            if (targetContract.IsEnumeration) {

                // create a generator
                var enumeration = targetXml.Elements().Select(
                    o => Deserialize(o, targetContract.ElementContract, null));

                // eagerly deserialize
                if (!deferDeserialization)
                    enumeration = enumeration.ToList();

                // cast the enumeration to the elementType                                
                var result = enumeration.Cast(targetContract.ElementContract.DeserializedType);
                return result;
            }

            // empty check
            if (!targetContract.CanBeEmpty && targetXml.Nodes().None()) {
                var error = "Element of type '" + targetContract.Name + "'";
                if (locationMember != null)
                    error += " in '" + locationMember + "'";
                throw Exception(targetXml, error += " cannot be empty.");
            }

            // simple (int, string, date, boolean, enum, etc)
            if (targetContract.IsSimple) {

                // test xml constraints
                targetContract.TestXmlConstraints(targetXml.Value);

                // deserializeXml
                var deserializedValue = targetContract.DeserializeXml(targetXml.Value);

                // test value constraints
                targetContract.TestConstraints(deserializedValue);

                // deserialize infoset
                var result = targetContract.Deserialize(deserializedValue);

                // use typed object if result could be more than one type of XsContract and user requests it
                if (typedObjectResult) {
                    KingDebug.Assert(locationIsObject || locationIsEnumeration);
                    result = TypedObject.Make(result, targetContract);
                }

                return result;
            } 
            
            // convert XAttributes into XElements associate a namespace with the attribute
            var preElementMatch =
                targetContract.Members().Where(o => o.IsAttribute)
                .LeftMatch(                                             // outter
                    targetXml.Attributes(),                             // inner
                    o => o.FullName,                                    // outter key selector
                    a => targetContract.Namespace + a.Name.LocalName,   // inner key selector
                    (o, a) => new { 
                        ContractMember = o,
                        XElement = a == null ? null : 
                            new XElement(targetContract.Namespace + a.Name.LocalName, (string)a)
                    });

            // append any simpleContent attributes
            if (targetContract.IsSimpleContent) {
                if (targetXml.HasElements)
                    throw Exception(targetXml, "SimpleContent cannot have child elements.");
            }

            // parallel walk of ContractMembers and XElements matched by XName
            // it's "left" because the XElement value may be missing
            // (e.g. will always happen when the the DatMember is a struct)
            var leftSequentialMatch = preElementMatch.Concat(targetContract.IsSimpleContent ? 
                new[] { new { ContractMember = targetContract.Members().Single(o => o.IsSimpleContent), XElement = targetXml } } : 
                targetContract.Members().Where(o => o.IsElement && !o.IsSimpleContent)
                    .LeftSequentialMatch(       // outter
                        targetXml.Elements(),   // inner
                        o => o.FullName,        // outter key selector
                        x => x.Name,            // inner key selector
                        (o, x) => new { ContractMember = o, XElement = x })).ToArray();

            // group elements that are not GroupContracts by order scoped to the declaring contract
            var groups = leftSequentialMatch
                .ExceptWhere(o => o.ContractMember.Contract.IsGroup)
                .GroupBy(o => new { 
                    o.ContractMember.Order,
                    o.ContractMember.DeclaringContract 
                });

            // test if more than once choice was made
            var moreThanOneChoice = groups.Where(o => o.Where(p => p.XElement != null).Count() > 1);
            if (moreThanOneChoice.Any())
                throw Exception(targetXml, "More than one choice was made: " + 
                    moreThanOneChoice.First().Where(o => o.XElement != null).StringJoin(", ", o => o.XElement.Name.LocalName));

            // test if a required choice was not made
            var noChoice = groups.Where(o => 
                o.Any(p => p.ContractMember.IsRequired) && // any choice was required...
                o.All(p => p.XElement == null ||     // ...and for all choices no element was supplied...
                        // ...or it was supplied but had no child text nodes...
                        (!p.XElement.Nodes().Any() && 
                         // ...and needed to be because primitives must have a value...
                         p.ContractMember.Contract.IsSimple && 
                         // ...except for string and its subclasses where <string /> maps to the empty string
                         !XsLoader.StringContract.IsReplaceableWith(p.ContractMember.Contract))
                    ));

            if (noChoice.Any()) {
                // convert to arrays for ease of debugger inspection
                var debug = noChoice.Select(o => o.ToArray()).ToArray();
                var message = "A value must be provided for '{0}'."
                    .Substitute(noChoice.First().StringJoin(", ", o => o.ContractMember));
                throw Exception(targetXml, message);
            }

            // deserialize
            var values = new List<object>();
            var members = new List<XsMember>();
            foreach (var pair in leftSequentialMatch) {
                XsMember member = pair.ContractMember;
                XsContract contract = member.Contract;
                XElement element = pair.XElement;

                object value = null;

                // simple content lives in the targetXml
                if (member.IsSimpleContent)
                    element = targetXml;

                // deserialize the element
                if (element != null) {

                    // dig out object element
                    if (contract.IsObject) {
                        if (!element.Elements().CountIs(1))
                            throw Exception(targetXml, "Object location must have exactly one element.");
                        element = element.Elements().SingleOrDefault();
                    }

                    value = Deserialize(element, contract, member);

                // deserialize all fields in the group
                } else if (contract.IsGroup) {
                    value = Deserialize(targetXml, contract, member);

                // provide a default value
                } else if (member.ProvideDefault)
                    value = member.Default;

                values.Add(value);
                members.Add(member);
            }

            // pair members with their values
            var zip = values.Zipper(members, (v, m) => new { Value = v, Member = m });

            // activate instance 
            var instance = targetContract.SerializedType.Activate(
               zip.Where(o => o.Member.IsActivationParameter).Select(o => o.Value));

            // set the field\property
            foreach (var o in zip) {
                var value = o.Value;

                // co-variance
                if (value != null && value.GetType().IsEnumerable()) {
                    var targetType = o.Member.PropertyOrFieldType.GetEnumerationElementType();
                    if (targetType != null && value.GetType().GetEnumerationElementType() != targetType)
                        value = ((IEnumerable)value).Cast(targetType);
                }

                o.Member.SetValue(instance, value);
            }

            // do custom deserialization
            if (targetContract.IsComplex)
                instance = targetContract.Deserialize(instance);

            return instance;
        }

        private object Serialize(
            // the object to be serialized
            object target, 
            // the Contract of the target 
            XsContract targetContract, 
            // the ContractMember into which we're storing the value
            XsMember targetContractMember,
            // the depth of the current xml node in the serialized result
            int depth) {

            KingDebug.Assert(target != null);
            KingDebug.Assert(targetContract != null);

            // metadata harvested from a targetContractMember when available
            var targetName = targetContract.FullName;
            var indentText = false;
            var useCData = false;
            var ignoreXmlRoot = false;
            var isAttribute = false;
            var locationIsObject = false;

            if (targetContractMember != null) {
                targetName = targetContractMember.DeclaringContract.Namespace + targetContractMember.SchemaName;
                indentText = targetContractMember.IndentText;
                useCData = targetContractMember.UseCDataSection;
                ignoreXmlRoot = targetContractMember.IgnoreXmlRoot;
                locationIsObject = targetContractMember.Contract.IsObject;
                isAttribute = targetContractMember.IsAttribute;
            }

            // object
            if (locationIsObject) {
                return new XElement(RenameSerialize(targetName), Serialize(target, targetContract, null, depth + 1));

            // simple
            } else if (targetContract.IsSimple) {

                target = targetContract.SerializeXml(target);

                // format the text so it is nicely indented
                if (indentText) 
                    target = Format(Environment.NewLine + target.ToString().Trim() + Environment.NewLine, depth, 1);

                // escape value
                if (useCData)
                    target = new XCData(target.ToString());

                // return attribute
                if (isAttribute)
                    return new XAttribute(targetName.LocalName, target);

                // return element
                return new XElement(RenameSerialize(targetName), target);

            // xml
            } else if (targetContract.IsXml) {
                var xmlTarget = (XElement)target;

                // clean up text nodes if the ContractMember will let us
                if (indentText)
                    foreach (var text in xmlTarget.DescendantNodes().Where(o => o is XText).Cast<XText>())
                        text.Value = Format(text.Value, depth + text.Ancestors().Count() - (ignoreXmlRoot ? 1 : 0), 0);

                // clear the default namespace
                return new XElement(targetName,
                    ignoreXmlRoot ? new XAttribute(XNamespace.Xmlns + "ad", targetName.NamespaceName) : null,
                    ignoreXmlRoot ? new XAttribute("xmlns", "") : null,
                    ignoreXmlRoot ? (object)xmlTarget.Nodes() : (object)xmlTarget);

            // enumeration
            } else if (targetContract.IsEnumeration) {

                var xml = new XElement(RenameSerialize(targetName));
                foreach (var element in ((IEnumerable)target)) {
                    var elementContract = targetContract.ElementContract.GetContract(element.GetType());
                    xml.Add(Serialize(element, elementContract, null, depth + 1));
                }
                return xml;

            // complex
            } else {
                var content = new List<object>();

                // serialize each field
                foreach (var contractMember in targetContract.Members()) {

                    var deserializedValue = contractMember.GetValue(target);

                    // fields with null values are not emitted
                    if (deserializedValue == null)
                        continue;

                    // get the Contract of the value
                    var contract = contractMember.GetContractOrNull(deserializedValue);
                    if (contract == null)
                        throw new ArgumentException("Failed to resolve a contract for the value stored in: " + contractMember);

                    // custom serialization
                    var serializedValue = contract.Serialize(deserializedValue);

                    // simpleContent
                    if (contractMember.IsSimpleContent) {
                        var simpleTarget = (XElement)Serialize(serializedValue, contract, contractMember, depth);
                        content.Add(simpleTarget.Nodes().First(o => o is XCData || o is XText));
                        content.Add(simpleTarget.Attributes());
                        content.Add(simpleTarget.Elements());

                    // attribute
                    } else if (contractMember.IsAttribute) {
                        var result = Serialize(serializedValue, contract, contractMember, depth + 1);
                        content.Add(result);

                    // element (or elements if contract is a group)
                    } else {
                        var result = Serialize(serializedValue, contract, contractMember, depth + 1);
                        content.Add(result);
                    }
                }

                var xml = new XElement(RenameSerialize(targetName), content);

                // Structs don't affect the structure of the XML
                if (targetContract.IsGroup)
                    return xml.Elements();

                return xml;
            }
        }

        private string Format(string text, int depth, int indent) {
            int tabSize = 2;

            var result = Regex.Replace(text, @"((^|\n)\s*)(?!$)",
                delegate(Match match) {
                    return match.Value.Replace("\t", "  ") + 
                        string.Empty.Indent(tabSize, depth + indent);
                });

            return result.TrimEnd() + Environment.NewLine + string.Empty.Indent(tabSize, depth);
        }
        #endregion

        #region Public Members
        public object Serialize(object target, XsContract contract) {

            if (contract == null)
                throw new ArgumentException();

            if (target is TypedObject)
                target = ((TypedObject)target).Object;

            var serializedTarget = contract.Serialize(target);

            var result = Serialize(
                serializedTarget,
                contract,
                null,       // targetContractMember
                0);         // depth

            return result;
        }
        public XElement Serialize(object target) {
            if (target == null)
                throw new ArgumentNullException();

            var assembly = XsAssembly.GetAssembly(Assembly.GetCallingAssembly());
            var contract = assembly.GetContract(target);

            return (XElement)Serialize(target, contract);
        }

        public object Deserialize(XElement xElement) {
            return Deserialize(xElement, typeof(object));
        }
        public T Deserialize<T>(XElement xElement) {
            return (T)Deserialize(xElement, typeof(T));
        }
        public object Deserialize(XElement xElement, Type type) {
            var contract = XsLoader.GetContractOrNull(type);
            if (type != null && contract == null)
                throw new ArgumentException();

            return Deserialize(xElement, contract, null);
        }
        #endregion
    }

    public class CsvBuilder {

        #region Private Data Members
        private StringBuilder m_stringBuilder;
        private bool m_firstColumn;
        private long m_value;
        private long m_count;
        #endregion

        #region Constructor
        public CsvBuilder(IEnumerable<string> names) {
            m_stringBuilder = new StringBuilder();
            m_firstColumn = true;

            m_count++;
            foreach (var name in names) {
                m_count++;
                AddValue(name);
            }
            m_count--;

            RowFinished();
        }
        #endregion

        #region Private Members
        private void RowFinished() {
            m_firstColumn = true;
            m_stringBuilder.Append(Environment.NewLine);
        }
        public static string EscapeCsv(string output) {
            bool special = false;
            if (output.Contains("\"")) {
                output = output.Replace("\"", "\"\"");
                special = true;
            }

            if (output.Contains(","))
                special = true;

            if (special)
                output = "\"" + output + "\"";

            return output;
        }
        #endregion

        #region Public Members
        public void AddValue(object value) {
            if (!m_firstColumn)
                m_stringBuilder.Append(",");

            m_firstColumn = false;
            if (value != null)
                m_stringBuilder.Append(EscapeCsv(value.ToString()));
            m_value++;
            if (m_value % m_count == 0)
                RowFinished();
        }
        public void AddValues(params object[] values) {
            foreach (var value in values)
                AddValue(value);
        }
        #endregion

        #region Object Overrides
        public override string ToString() {
            return m_stringBuilder.ToString();
        }
        #endregion
    }

    [XsComplexContract]
    public class XsCsv : XsAnyType {

        #region Private Web Data Members
        [XsMember(IsSimpleContent = true, UseCDataSection = true)]
        public string Value { get; set; }

        [XsMember(Order = 1, IsAttribute = true)]
        public string Type { get; set; }
        #endregion

        #region Constructor
        public XsCsv() {
        }
        public XsCsv(XsContract rowContract, IEnumerable<XsMember> members, IEnumerable rows) {
            Type = rowContract.Name;

            var csv = new CsvBuilder(members.Select(o => o.SchemaName));

            foreach (var row in rows) {
                foreach (var member in members) {
                    var contract = member.Contract;

                    var deserializedValue = member.GetValue(row);
                    var serializedValue = contract.Serialize(deserializedValue);
                    var csvValue = contract.SerializeCsv(serializedValue);
                    csv.AddValue(csvValue);
                }
            }

            Value = csv.ToString();
        }
        #endregion
    }
}
